package com.magenta.mrs.geocoding.connector;

import com.magenta.maxoptra.gis.GeoException;
import com.magenta.mrs.geocoding.mapping.XmlMappingFunctor;
import com.magenta.mrs.util.GeoInformationClient;

import org.apache.http.*;
import org.apache.http.auth.*;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.URL;

/**
 * Address Standardizer class.
 * <p/>
 * Note: The http connection is synchronized in this class!
 * you need to create multiple standardizer if you need concurrency.
 * <br/>
 * This class provides a set of methods for standardizing an address.
 * <br/>
 * <p>
 * Note that this class standardizes the input address by sending a http request to
 * google's geocoder service (http://www.google.com/apis/maps/documentation/).
 * This service requires an ApiKey which you need to sign up for before you can use this class.
 * </p>
 * <p>
 * There is a geocoding speed limit (from http://googlemapsapi.blogspot.com/2007/09/coming-soon-ip-based-geocode-limiting.html):
 * <p>
 * "In the coming week, the Maps API geocode limit will change from a key-based system to an IP-based system, with a new limit of 15,000 queries per day. If you're a developer with a website that's using client-side geocoding via the GClientGeocoder object, this change means that each of your website visitors will now be subject to their own 15K quota. However, if you're a developer using the HTTP geocoder, this change means that all the geocodes from your script will be subject to the same 15K quota (your web server will send the same IP to us with each geocode). We've made this change in our geocoder due to the number of developers who've had issues with the GClientGeocoder and going over quota in times of high mashup user volume."
 * </p>
 * That means if you run at a rate faster than the equivalent of
 * 15000 requests per day (5.769 seconds per request) <b>per IP address</b> for several minutes, then Google
 * will block you for a day. <b>This class automatically enforces this limit by only sending out
 * request in 5.769 second interval</b>. You can change the value of this time interval via the constructor.
 * </p>
 * <pre>
 *  long timeTilNextStart = _rateLimitInterval - ( System.currentTimeMillis() - _lastRequestTime);
 *  if(timeTilNextStart > 0){
 *      Thread.sleep(timeTilNextStart); //sleep for some time
 *  }
 *  _lastRequestTime = System.currentTimeMillis();
 * </pre>
 * For more information about this service, see http://www.google.com/apis/maps/index.html
 *
 * @author jliang
 */
public class GeoAddressStandardizer {
    //    private static final String BASE_URL = "http://maps.google.com/maps/geo?q={0}&output={1}&key={2}";
    //    private static final String XML = "xml", CSV = "csv";
    //    private String _apiKey;
    private boolean canSleepDueToGeocodingLimit = true;
    private long _rateLimitInterval = 5769L;
    private long _lastRequestTime = System.currentTimeMillis() - _rateLimitInterval;
    private HttpClientParams _httpClientParams = null;

    //private static HttpConnectionManager _connectionManager = new MultiThreadedHttpConnectionManager();

    ///**
    // * The httpClient in combination with the {@link org.apache.commons.httpclient.MultiThreadedHttpConnectionManager} is
    // * thread-safe. See: <a href="http://hc.apache.org/httpclient-3.x/threading.html">HttpClient - Threading</a>
    // */
    //private static HttpClient _httpClient = new HttpClient(_connectionManager);
    private static HttpClient _httpClient;

    private HttpClient client;

    static {
        HttpParams params = new BasicHttpParams();
        SchemeRegistry reg = new SchemeRegistry();
        SocketFactory socketFactory = new PlainSocketFactory();
        Scheme httpScheme = new Scheme("http", socketFactory, 80);
        reg.register(httpScheme);
        ClientConnectionManager connManager = new ThreadSafeClientConnManager(params, reg);
        _httpClient = new DefaultHttpClient(connManager, params);
    }

//    /**
//     * Sets the {@link org.apache.commons.httpclient.HttpConnectionManager} to be used for connecting to the geocoding service
//     */
//    public static synchronized void setConnectionManager(HttpConnectionManager manager) {
//        _connectionManager = manager;
//        _httpClient = new HttpClient(_connectionManager);
//    }

    public void setCanSleepDueToGeocodingLimit(boolean canSleepDueToGeocodingLimit) {
        this.canSleepDueToGeocodingLimit = canSleepDueToGeocodingLimit;
    }

    ///**
    // * Sets the {@link org.apache.commons.httpclient.HttpClient} to be used for connecting to the geocoding service
    // *
    // * @param client
    // */
    public static synchronized void setHttpClient(HttpClient client) {
        _httpClient = client;
    }

    /**
     * Parameters for controlling the http connection.
     * http://jakarta.apache.org/commons/httpclient/preference-api.html#HTTP_parameters
     *
     * @return
     */
    public HttpClientParams getHttpClientParams() {
        return _httpClientParams;
    }

//    public void setHttpClientParams(HttpClientParams httpClientParams) {
//        _httpClientParams = httpClientParams;
//        if (_httpClientParams != null && _httpClient != null) {
//            _httpClient.setParams(_httpClientParams);
//        }
//    }

    public GeoAddressStandardizer() {
    }

    public static class PreemptiveAuth implements HttpRequestInterceptor {

        final private HttpHost proxy;

        public PreemptiveAuth(HttpHost proxy) {
            this.proxy = proxy;
        }

        public void process(
                final HttpRequest request,
                final HttpContext context) throws HttpException, IOException {

            AuthState authState = (AuthState) context.getAttribute(
                    ClientContext.TARGET_AUTH_STATE);
//            AuthState authState = (AuthState) context.getAttribute(
//                    ClientContext.PROXY_AUTH_STATE);

            // If no auth scheme avaialble yet, try to initialize it preemptively
            if (authState.getAuthScheme() == null) {
                BasicScheme basicAuth = new BasicScheme();
                context.setAttribute("preemptive-auth", basicAuth);
                AuthScheme authScheme = (AuthScheme) context.getAttribute(
                        "preemptive-auth");
                CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(
                        ClientContext.CREDS_PROVIDER);
                HttpHost targetHost = (HttpHost) context.getAttribute(
                        ExecutionContext.HTTP_TARGET_HOST);
                if (authScheme != null) {
                    Credentials creds = credsProvider.getCredentials(
                            new AuthScope(
                                    proxy.getHostName(),
                                    proxy.getPort()));
                    if (creds == null) {
                        throw new HttpException("No credentials for preemptive authentication");
                    }
                    authState.setAuthScheme(authScheme);
                    authState.setCredentials(creds);
                }
            }
        }
    }

    protected HttpClient getClient() {
    	return this.client != null ? this.client : _httpClient;
    }

    public GeoAddressStandardizer(final Credentials credentials, final String proxyHost, final int proxyPort) {
        this();

        if (credentials != null) {
            //HttpState state = _httpClient.getState();
            //state.setProxyCredentials(AuthScope.ANY, credentials);
            //state.setAuthenticationPreemptive(true);

            //_httpClient.getHostConfiguration().setProxy(proxyHost, proxyPort);
            Object oldProxy = _httpClient.getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY);
            HttpHost proxy = new HttpHost(proxyHost, proxyPort);
            if (!proxy.equals(oldProxy)) {
                _httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
                ((DefaultHttpClient) _httpClient).getCredentialsProvider().setCredentials(
                        new AuthScope(proxyHost, proxyPort),
                        credentials);
                BasicHttpContext localContext = new BasicHttpContext();
                BasicScheme basicAuth = new BasicScheme();
                localContext.setAttribute("preemptive-auth", basicAuth);

                ((DefaultHttpClient) _httpClient).addRequestInterceptor(new PreemptiveAuth(proxy), 0);
            }
        }

    }


    public <ReturnType> ReturnType standardize(String urlString, XmlMappingFunctor<ReturnType> mappingFunction) throws GeoException {
        try {
//            HttpURL url = new HttpURL(urlString);
            URL url = new URL(urlString.replaceAll(" ", "%20"));
            String res = getServerResponse(url.toString());
            return mappingFunction.execute(res);
        }
        catch (RuntimeException re) {
            throw re;
        } catch (GeoException e) {
            throw e;
        } catch (Exception e) {
            throw new GeoException(e.getMessage());
        }
    }

    private synchronized String getServerResponse(String url) throws Exception {
        //GetMethod get = null;
        HttpGet get = null;
        try {
            if (canSleepDueToGeocodingLimit) {
                long timeTilNextStart = _rateLimitInterval - (System.currentTimeMillis() - _lastRequestTime);
                if (timeTilNextStart > 0) {
                    Thread.sleep(timeTilNextStart); //sleep for some time
                }
            }

            //Setup connection timeout (aka socket timeout)...
            //_httpClient.getHttpConnectionManager().getParams().setConnectionTimeout((int) _rateLimitInterval);
            getClient().getParams().setIntParameter("http.connection.timeout", (int) _rateLimitInterval);
            _lastRequestTime = System.currentTimeMillis();
            //get = new GetMethod(url);
            get = new HttpGet(url);
            //get.setFollowRedirects(true);
            //_httpClient.executeMethod(get);
            HttpResponse resp = getClient().execute(get);
            if (resp.getStatusLine().getStatusCode() == 407) {
                throw new AuthenticationException("INVALID LOGIN OR PASSPHRASE FOR PROXY.");
            }
            HttpEntity entity = resp.getEntity();
            if (entity != null) {
                //long len = entity.getContentLength();
                //if (len != -1 && len < 2048) {
                    return EntityUtils.toString(entity);
                //}
            }
            return "";
            //return IOUtils.toString(get.getResponseBodyAsStream(), get.getRequestCharSet());
        } finally {
            //if (get != null) get.releaseConnection();
            if (get != null) get.abort();
        }
    }


    public long getRateLimitInterval() {
        return _rateLimitInterval;
    }

    public void setRateLimitInterval(long rateLimitInterval) {
        _rateLimitInterval = rateLimitInterval;
    }

	public void setClient(GeoInformationClient client) {
		this.client = client;
	}


}